Skip to main content

Using the Enhanced Input System

In game development, handling user input is a crucial part, especially when dealing with complex input sequences or Quick Time Events (QTEs). Dora SSR provides an enhanced input system that allows developers to manage various input events more efficiently and flexibly. This tutorial will guide you on how to set up and use this input system, explaining the new concepts involved in detail.

1. New Concepts Involved

Dora SSR's enhanced input system allows you to create complex input logic, such as multi-stage QTEs and combo keys. By using input contexts, actions, and triggers, you can precisely control how the game responds to player inputs in different states.

1.1 Action

An action is the basic unit in the input system that defines a set of conditions under which a behavior is triggered. For example, pressing the confirm key for confirmation, or pressing the movement key to move a character.

1.2 Input Context

An input context is a collection of actions that allows you to activate or deactivate a set of input actions based on the game scene. For instance, in a game menu scene, you may only need to handle navigation and selection inputs. In an in-game scene, you might need to handle a different set of inputs, such as movement and attack.

1.3 Trigger

A trigger defines the conditions under which an action is activated. It can be a simple key press or a complex input sequence. Dora SSR provides various types of triggers, including:

  • KeyDown: Triggered when all specified keys are pressed.
  • KeyUp: Triggered when all specified keys are pressed and any one of them is released.
  • KeyPressed: Triggered when all specified keys are currently pressed.
  • KeyHold: Triggered when a specific key is pressed and held for a specified duration.
  • KeyTimed: Triggered when a specific key is pressed within a specified time window.
  • KeyDoubleDown: Triggered when a specific key is double-clicked.
  • AnyKeyPressed: Triggered when any key is continuously pressed.
  • ButtonDown: Triggered when all specified game controller buttons are pressed.
  • ButtonUp: Triggered when all specified game controller buttons are pressed and any one of them is released.
  • ButtonPressed: Triggered when all specified game controller buttons are currently pressed.
  • ButtonHold: Triggered when a specific game controller button is pressed and held for a specified duration.
  • ButtonTimed: Triggered when a specific game controller button is pressed within a specified time window.
  • ButtonDoubleDown: Triggered when a specific game controller button is double-clicked.
  • AnyButtonPressed: Triggered when any game controller button is continuously pressed.
  • JoyStick: Triggered when a specific game controller axis is moved.
  • JoyStickThreshold: Triggered when the joystick moves beyond a specified threshold.
  • JoyStickDirectional: Triggered when the joystick moves in a specific direction within a tolerance angle.
  • JoyStickRange: Triggered when the joystick is within a specified range.
  • Sequence: Requires triggers to be detected in a specific order.
  • Selector: Triggers the action as long as any one trigger is activated.
  • Block: Prevents other triggers from being activated.

1.4 Trigger State

When a trigger is activated, it will trigger a corresponding global event in the engine, which contains the current state of the trigger. There are three trigger states:

  • Ongoing: The trigger condition is in progress.
  • Completed: The trigger condition has been completed.
  • Canceled: The trigger condition has been canceled.

1.5 Relationship between Contexts, Actions, Triggers, and Trigger States

An input context contains multiple actions, each action contains a tree-structured organization of triggers, which provide various trigger event sources and the current input state.

1.6 Nesting of Triggers

Here is an example of a tree-nested trigger definition used to describe a trigger for pressing the Ctrl key and the C key simultaneously:

The corresponding trigger code definition:

Trigger.Sequence({
Trigger.KeyPressed("LCtrl"),
Trigger.KeyDown("C")
})

Here is a trigger definition for pressing and holding the keyboard Enter key or the game controller A button for 1 second to trigger a confirmation action:

The corresponding trigger code definition:

Trigger.Selector({
Trigger.KeyHold("Return", 1),
Trigger.ButtonHold("a", 1)
})

2. Creating the Input System

2.1 Simple Input System Example 1

Here is a simple code example for creating an input system:

-- Import modules
local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger
local Node <const> = require("Node")

-- Create input manager with one context and one action
local input = InputManager.CreateManager({
testContext = {
["Ctrl+C"] = Trigger.Sequence({
Trigger.KeyPressed("LCtrl"),
Trigger.KeyDown("C")
})
}
})

-- Create a node to receive and process input events
local node = Node()

-- Connect global event signals; note the "Input." prefix matches the action's name
node:gslot("Input.Ctrl+C", function(state, progress, value)
if state == "Completed" then
print("Ctrl+C triggered successfully")
-- Remove the current active context, pressing Ctrl+C won't trigger again
input:popContext()
end
end)

-- Activate the testContext to enable its input triggers
input:pushContext("testContext")

In this example, we created an input manager, defined an input context, and one action. The action Ctrl+C trigger defined the conditions for pressing the Ctrl key and the C key. We pushed this context into the input manager for activation. Then we created a scene node to receive and process input events. Finally, we connected the corresponding global event signals, printing a message when the action Ctrl+C is completed and removing the current active context.

Tip

When registering global event signals for handling input events, use the Input. prefix followed by the action's name, such as Input.Confirm. Note that in the global event callback function, we can retrieve the trigger's state (state), progress (progress), and value (value). In this example, we only handled the completion state of the action. When using triggers related to time (Hold or Timed), we can obtain the current progress of the trigger through the progress parameter (ranging from 0 to 1). When using triggers that provide varying input values (like joystick axis input), we can retrieve the current input value through the value parameter (value).

2.2 Simple Input System Example 2

Here is another simple input system example, including a long-press confirmation UI interaction context and a game scene context for character movement:

local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger
local Node <const> = require("Node")

-- Create input manager with two contexts and their actions
local inputManager = InputManager.CreateManager({
UI = {
Confirm = Trigger.Selector({
Trigger.KeyHold("Return", 1),
Trigger.ButtonHold("a", 1)
})
},
Game = {
MoveLeft = Trigger.Selector({
Trigger.KeyPressed("Left"),
Trigger.ButtonPressed("dpleft")
}),
MoveRight = Trigger.Selector({
Trigger.KeyPressed("Right"),
Trigger.ButtonPressed("dpright")
})
}
})

-- Create a node to receive and process input events
local node = Node()

-- Connect global event signals to handle the confirm action in the UI context
node:gslot("Input.Confirm", function(state, progress)
if state == "Ongoing" then
print(string.format("Confirming, progress: %d", progress * 100))
elseif state == "Completed" then
print("Confirmation complete")
end
end)

-- Connect global event signals to handle movement actions in the Game context
node:gslot("Input.MoveLeft", function(state)
if state == "Completed" then
print("Moving left")
end
end)

node:gslot("Input.MoveRight", function(state)
if state == "Completed" then
print("Moving right")
end
end)

In this example, we created an input manager that includes two contexts: UI and Game. The UI context contains a long-press confirmation action Confirm, while the Game context contains two movement actions MoveLeft and MoveRight. We created a node to receive and process input events. We then connected the global event signals to handle the confirm action in the UI context and the movement actions in the Game context.

When handling the confirm action in the UI context, we can also retrieve the current trigger state and the long-press progress. When handling the movement actions in the Game context, we only need to handle the action completion state.

In actual games, we can dynamically activate or deactivate different input contexts based on the current game state to achieve different input logic. When needing to activate or deactivate a context, simply call the pushContext or popContext methods.

-- Assuming we are currently in a game operation scene
-- Activate the Game context to start handling character movement
inputManager:pushContext("Game")

-- Assuming we need to open a UI interface for a confirmation operation
-- Activate the UI context, automatically deactivating the Game context
inputManager:pushContext("UI")

-- Assuming the UI interface is now closed
-- Deactivate the UI context, then the remaining Game context on the stack will be reactivated
inputManager:popContext()

-- Assuming you need to activate both Game and UI contexts simultaneously to accept two types of input
inputManager:pushContext({"UI", "Game"})

-- Popping the context from the top of the stack
-- Will deactivate the just activated group of two contexts
inputManager:popContext()

In this example, we demonstrated how to dynamically activate or deactivate different input contexts to switch between different input logics.

Tip

Only the context at the top of the input manager stack will be effective, while contexts not at the top will be automatically deactivated. This mechanism helps you keep track of historical input contexts for reactivation when needed.

3. Implementing Complex Input Logic

In the previous examples, we created a simple input system with one context and one action. Now, we will delve into how to use triggers to implement more complex input logic, such as multi-stage Quick Time Events (QTE).

3.1 Defining QTE Context

To implement multi-stage QTEs, we can create a function to generate the input context for each stage. Each stage has specific keys or buttons and corresponding time windows.

local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger

-- Function to define a QTE challenge input context supporting both keyboard and game controller buttons
local function QTEContext(keyName, buttonName, timeWindow)
return {
QTE = Trigger.Sequence({
Trigger.Selector({
-- Trigger for filtering specific keyboard keys
-- Trigger failure on pressing the wrong key
Trigger.Selector({
Trigger.KeyPressed(keyName),
Trigger.Block(Trigger.AnyKeyPressed())
}),
-- Trigger for filtering specific game controller buttons
-- Trigger failure on pressing the wrong button
Trigger.Selector({
Trigger.ButtonPressed(buttonName),
Trigger.Block(Trigger.AnyButtonPressed())
})
}),
-- Trigger to detect pressing the specified key or button within the designated time window
Trigger.Selector({
Trigger.KeyTimed(keyName, timeWindow),
Trigger.ButtonTimed(buttonName, timeWindow)
})
})
}
end

In this function:

  • contextName: The name of the context used to identify the current QTE stage.
  • keyName: The name of the specified keyboard key.
  • buttonName: The name of the specified game controller button.
  • timeWindow: The time window within which the input must be completed, measured in seconds.

The trigger uses Trigger.Sequence to combine triggers, requiring only the specified key or button to be pressed, otherwise triggering a failure. As long as the correct key or button is pressed within the designated time window, it will trigger successfully.

3.2 Creating Input Manager and Adding QTE Context

Now, we will create an input manager and add the default context along with multiple QTE stage contexts.

-- Create input manager and add contexts
local inputManager = InputManager.CreateManager({
Default = {
StartQTE = Trigger.Selector({
Trigger.KeyDown("Space"),
Trigger.ButtonDown("start")
})
},
-- Add QTE stage contexts
Phase1 = QTEContext("J", "a", 3), -- Phase 1: Press key J or button A within 3 seconds
Phase2 = QTEContext("K", "b", 2), -- Phase 2: Press key K or button B within 2 seconds
Phase3 = QTEContext("L", "x", 1) -- Phase 3: Press key L or button X within 1 second
})

-- Activate the default context
inputManager:pushContext("Default")

Here, we:

  • Created a default context containing the action StartQTE to initiate the QTE.
  • Used the QTEContext function to add three QTE stage contexts: Phase1, Phase2, and Phase3.

3.3 Handling Input Events

Next, we need to handle input events, particularly the QTE logic. We will create a node and connect the corresponding global event signals.

local Node <const> = require("Node")

-- Create node
local node = Node()

-- Define the current QTE phase
local phase = "None"
local contextCount = 1

-- Logic to handle moving to the next QTE phase
local function nextPhase()
-- 1 -> 2
if phase == "Phase1" then
phase = "Phase2"
print("Press key K or button B")
-- 2 -> 3
elseif phase == "Phase2" then
phase = "Phase3"
print("Press key L or button X")
-- 3 -> End
elseif phase == "Phase3" then
phase = "None"
inputManager:popContext(contextCount)
print("Challenge successful!")
return
end

-- Activate the next phase context
inputManager:pushContext(phase)
contextCount = contextCount + 1
end

-- Handle input event to start the QTE challenge
node:gslot("Input.StartQTE", function(state)
if state == "Completed" then
phase = "Phase1"
print("Press key J or button A")
inputManager:pushContext(phase)
contextCount = contextCount + 1
end
end)

-- Handle input events for the QTE
node:gslot("Input.QTE", function(state, progress)
if state == "Ongoing" then
-- Handle the countdown progress of the QTE; progress is a value increasing from 0 to 1
-- print(string.format("Progress: %.2f", progress))
elseif state == "Canceled" then
if phase ~= "None" then
phase = "None"
inputManager:popContext(contextCount)
print("Challenge failed!")
end
elseif state == "Completed" then
nextPhase()
end
end)

print("Press space bar or start button to initiate QTE challenge")

In this code:

  • Handle the global event Input.StartQTE: When the action to start the QTE is completed, it enters the Phase1 stage and activates the corresponding context.
  • Handle the global event Input.QTE: Based on the current phase, it processes the QTE logic. When the trigger completes, it moves to the next phase; when the trigger is canceled, it indicates failure.

4. Summary

Through this tutorial, you have learned how to use Dora SSR's enhanced input system to create complex input logic, including multi-stage Quick Time Events (QTEs). We explored the concepts of input contexts, actions, and triggers, as well as how to use them to precisely control the game's responses in different states.

Next, you can try applying this knowledge in your own projects to create richer and more interactive gaming experiences.